/*
COPYRIGHT (C) 2008 EPISERVER AB

THIS FILE IS PART OF SCRUM DASHBOARD.

SCRUM DASHBOARD IS FREE SOFTWARE: YOU CAN REDISTRIBUTE IT AND/OR MODIFY IT UNDER THE TERMS OF 
THE GNU LESSER GENERAL PUBLIC LICENSE VERSION v2.1 AS PUBLISHED BY THE FREE SOFTWARE FOUNDATION.

SCRUM DASHBOARD IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, BUT WITHOUT ANY WARRANTY; WITHOUT
EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. SEE THE GNU LESSER
GENERAL PUBLIC LICENSE FOR MORE DETAILS.

YOU SHOULD HAVE RECEIVED A COPY OF THE GNU LESSER GENERAL PUBLIC LICENSE ALONG WITH SCRUM DASHBOARD. 
IF NOT, SEE <HTTP://WWW.GNU.ORG/LICENSES/>.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Configuration;
using System.IO;
using System.Net;
using System.Text;
using System.Web;
using System.Web.Caching;
using System.Xml;
using System.Linq;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Common;
using Microsoft.TeamFoundation.Server;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using ScrumDashboard.Library.Model;

namespace ScrumDashboard.Library.Tfs
{
    /// <summary>
    /// Handles all communication with TFS
    /// </summary>
    public partial class TfsManager
    {
        private bool _viewBugsAsProductBacklogs;
        private TeamFoundationServer _tfs;
        private readonly static object _cachedTFSManager = new object();
        private Dictionary<string, User> _userLookup;

        public TfsManager()
        {
            _viewBugsAsProductBacklogs = ConfigurationManager.AppSettings["tfsViewBugsAsProductBacklogs"] == null ? true : Boolean.Parse(ConfigurationManager.AppSettings["tfsViewBugsAsProductBacklogs"]);

        }

        /// <summary>
        /// Get all defined sprints in current project
        /// </summary>
        /// <returns></returns>
        public SprintItem[] GetSprints(string tfsProject)
        {
            string cacheKey = tfsProject + "-GetSprints-";
            SprintItem[] sprints = HttpRuntime.Cache[cacheKey] as SprintItem[];
            if (sprints == null)
            {
                Hashtable context = new Hashtable();
                context.Add("project", tfsProject);
                WorkItemStore workItemStore = GetService<WorkItemStore>();

                WorkItemCollection retVal = workItemStore.Query("SELECT " + ObjectFactory.SprintItemFields + " FROM WorkItem WHERE [System.TeamProject] = @project AND [System.WorkItemType]='Sprint' AND [System.State]<>'Deleted' ORDER BY [Conchango.TeamSystem.Scrum.SprintStart] DESC,[System.CreatedDate] DESC", context);
                sprints = new SprintItem[retVal.Count];
                for (int i = 0; i < sprints.Length; i++)
                {
                    WorkItem sprintSource = retVal[i];
                    SprintItem sprint = ObjectFactory.CreateSprintItem(sprintSource);
                    sprints[i] = sprint;
                }
                HttpRuntime.Cache.Add(cacheKey, sprints, new TfsCacheDependency(tfsProject), DateTime.Now.AddHours(1), Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
            }
            return sprints;
        }

        /// <summary>
        /// Get all defined teams in current project
        /// </summary>
        /// <returns></returns>
        public string[] GetScrumTeams(string tfsProject)
        {
            string cacheKey = tfsProject + "-GetScrumTeams";
            string[] cachedTeams = HttpRuntime.Cache[cacheKey] as string[];
            if (cachedTeams == null)
            {
                List<string> teams = new List<string>();
                WorkItemStore workItemStore = GetService<WorkItemStore>();
                XmlDocument doc = workItemStore.ExportGlobalLists();
                XmlNode node = doc.SelectSingleNode("//GLOBALLIST[@name='Team (Scrum) - " + tfsProject + "']");
                if (node == null)
                    return new string[] { };

                foreach (XmlNode listItem in node.ChildNodes)
                {
                    teams.Add(listItem.Attributes["value"].Value);
                }

                cachedTeams = teams.OrderBy(t => t).ToArray<string>();

                HttpRuntime.Cache.Add(cacheKey, cachedTeams, null, DateTime.Now.AddHours(1), Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Default, null);
            }

            return cachedTeams;
        }

        /// <summary>
        /// Gets a list of display names for all users that are member of the project group Contributors
        /// </summary>
        /// <param name="tfsProject">The project in TFS</param>
        /// <returns>A list of users</returns>
        public IList<string> GetProjectContributors(string tfsProject)
        {
            string cacheKey = tfsProject + "-GetProjectContributors";
            IList<string> contributors = HttpRuntime.Cache[cacheKey] as IList<string>;

            if (contributors == null)
            {
                Dictionary<string, User> lookup = GetProjectContributorsLookup(tfsProject);

                contributors = (from l in lookup
                                orderby l.Key
                                select l.Key).ToList<string>().AsReadOnly();

                HttpRuntime.Cache.Add(cacheKey, contributors, null, DateTime.Now.AddHours(1), Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Default, null);
            }

            return contributors;
        }

        /// <summary>
        /// Move work item into a new state
        /// </summary>
        /// <param name="workItemID"></param>
        /// <param name="state"></param>
        /// <remarks>Will throw an exception for invalid states</remarks>
        public void MoveWorkItemToState(int workItemID, string state)
        {
            MoveWorkItemToState(workItemID, state, null);
        }

        /// <summary>
        /// Move work item into a new state
        /// </summary>
        /// <param name="workItemID"></param>
        /// <param name="state"></param>
        /// <remarks>Will throw an exception for invalid states</remarks>
        public User MoveWorkItemToState(int workItemID, string state, string assignedTo)
        {
            WorkItemStore workItemStore = GetService<WorkItemStore>();

            WorkItem wi = workItemStore.GetWorkItem(workItemID);
            wi.State = state;
            if (!String.IsNullOrEmpty(assignedTo))
                wi.Fields["System.AssignedTo"].Value = assignedTo;
            wi.Save();

            TfsCacheDependency.FireTfsChanged(wi.Project.Name);//To make sure it's fired before the AJAX callback

            return GetUser(wi.GetTeamProject(),assignedTo);
        }

        /// <summary>
        /// The product backlog for a specific sprint number
        /// </summary>
        /// <param name="sprintNumber">The number of the sprint to get backlog for</param>
        /// <param name="teamName">Filter by team (optional)</param>
        /// <returns>A list of Product Backlog items</returns>
        public IList<ProductBacklogItem> GetProductBacklog(string tfsProject, int sprintNumber, string teamName, bool itemsNotInSprint)
        {
            string cacheKey = tfsProject + "-GetProductBacklog-" + sprintNumber.ToString() + "-" + teamName + "-" + itemsNotInSprint.ToString();
            IList<ProductBacklogItem> cachedItems = HttpRuntime.Cache[cacheKey] as IList<ProductBacklogItem>;
            if (cachedItems == null)
            {
                Hashtable context = new Hashtable();
                context.Add("project", tfsProject);
                context.Add("sprintNumber", sprintNumber);
                context.Add("team", teamName);
                WorkItemStore workItemStore = GetService<WorkItemStore>();
                WorkItemCollection retVal;

                string productBacklogTypes = _viewBugsAsProductBacklogs ? "[System.WorkItemType]='Product Backlog Item' OR [System.WorkItemType]='Bug'" : "[System.WorkItemType]='Product Backlog Item'";

                string baseQ = "SELECT " + ObjectFactory.ProductBacklogFields + " FROM WorkItems WHERE [System.TeamProject] = @project AND (" + productBacklogTypes + ") AND [System.State]<>'Deleted'";
                if (!String.IsNullOrEmpty(teamName))
                    baseQ += " AND [Conchango.TeamSystem.Scrum.team]=@team";
                if (itemsNotInSprint)
                    baseQ += " AND [System.IterationId]<>@sprintNumber AND [System.State]<>'Done' ORDER BY [Conchango.TeamSystem.Scrum.BusinessPriority] DESC";
                else
                    baseQ += " AND [System.IterationId]=@sprintNumber ORDER BY [Conchango.TeamSystem.Scrum.BusinessPriority] DESC,[Conchango.TeamSystem.Scrum.DeliveryOrder] DESC";

                retVal = workItemStore.Query(baseQ, context);

                List<ProductBacklogItem> items = new List<ProductBacklogItem>();
                for (int i = 0; i < retVal.Count; i++)
                {
                    WorkItem pbiSource = retVal[i];
                    ProductBacklogItem pbi = ObjectFactory.CreateProductBacklogItem(pbiSource, GetUser(tfsProject, pbiSource.Fields["System.AssignedTo"].Value));

                    items.Add(pbi);
                }

                cachedItems = items.AsReadOnly();
                HttpRuntime.Cache.Add(cacheKey, cachedItems, new TfsCacheDependency(tfsProject), DateTime.Now.AddHours(1), Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
            }

            return cachedItems;
        }

        public SprintViewData GetSprintViewData(string tfsProject, int sprintNumber, string teamName)
        {
            string cacheKey = tfsProject + "-GetSprintViewData-" + sprintNumber.ToString() + "-" + teamName;
            SprintViewData view = HttpRuntime.Cache[cacheKey] as SprintViewData;
            if (view == null)
            {
                view = new SprintViewData();
                view.Sprint = GetSprint(sprintNumber);
                view.ProductBacklog = GetProductBacklog(tfsProject, sprintNumber, teamName, false).ToList();
                view.SprintBacklog = CreateSprintViewBacklog(tfsProject, sprintNumber, teamName, view.ProductBacklog);

                view.ProductBacklog = new ReadOnlyCollection<ProductBacklogItem>(view.ProductBacklog);
                view.SprintBacklog = new ReadOnlyCollection<SprintBacklogItem>(view.SprintBacklog);
                view.Statistics = SprintViewStatistics.ComputeStatistics(view);

                HttpRuntime.Cache.Add(cacheKey, view, new TfsCacheDependency(tfsProject), DateTime.Now.AddHours(1), Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
            }
            return view;
        }

        /// <summary>
        /// Get the workitem for a specific sprint number
        /// </summary>
        /// <param name="sprintNumber">The sprint number</param>
        /// <returns>The work item representing the sprint</returns>
        public SprintItem GetSprint(int sprintNumber)
        {
            string cacheKey = "GetSprint-" + sprintNumber.ToString();
            SprintItem sprint = HttpRuntime.Cache[cacheKey] as SprintItem;
            if (sprint == null)
            {
                Hashtable context = new Hashtable();
                context.Add("sprintNumber", sprintNumber);
                WorkItemStore workItemStore = GetService<WorkItemStore>();
                WorkItemCollection retVal =
                    workItemStore.Query(
                        "SELECT " + ObjectFactory.SprintItemFields + " FROM WorkItems WHERE [System.WorkItemType]='Sprint' AND [System.IterationId]=@sprintNumber AND [System.State]<>'Deleted'",
                        context);
                if (retVal.Count == 0)
                    return null;

                sprint = ObjectFactory.CreateSprintItem(retVal[0]);
                if (sprint == null)
                    return null;
                HttpRuntime.Cache.Add(cacheKey, sprint, new TfsCacheDependency(retVal[0].Project.Name), DateTime.Now.AddHours(1), Cache.NoSlidingExpiration, CacheItemPriority.Default, null);

            }

            return sprint;
        }

        /// <summary>
        /// Get a list of all project in TFS that has a work item type
        /// </summary>
        /// <param name="witNames">A list of required Work Item Types</param>
        /// <returns>A list of projects</returns>
        public Project[] GetProjectsWithWIT(params string[] witNames)
        {
            ArrayList projs = new ArrayList();
            WorkItemStore workItemStore = GetService<WorkItemStore>();

            foreach (Project proj in workItemStore.Projects)
            {
                if (!proj.HasWorkItemReadRights)
                    continue;

                //This code supports empty witNames collection, not obvious
                bool failedToFindType = false;
                foreach (string witName in witNames)
                {
                    if (!proj.WorkItemTypes.Contains(witName))
                    {
                        failedToFindType = true;
                        break;
                    }
                }
                if (!failedToFindType)
                    projs.Add(proj);
            }
            return (Project[])projs.ToArray(typeof(Project));
        }

        /// <summary>
        /// Get a list of all project in TFS that are "Scrum for Team System"
        /// </summary>
        /// <returns>A list of projects</returns>
        public Project[] GetScrumProjects()
        {
            //WIT "Impediment" is non-existing in v1 of Scrum
            return GetProjectsWithWIT("Sprint", "Impediment");
        }

        public ProductBacklogItem GetProductBacklog(int id)
        {
            return GetProductBacklog(id, 0);
        }

        public ProductBacklogItem GetProductBacklog(int id, int revision)
        {
            WorkItem wi = revision > 0 ? GetWorkItem(id, revision) : GetWorkItem(id);
            return ObjectFactory.CreateProductBacklogItem(wi, GetUser(wi.Project.Name, wi.Fields["System.AssignedTo"].Value));
        }

        /// <summary>
        /// Get a work item for a specific id
        /// </summary>
        /// <param name="id">The Work Item ID</param>
        /// <returns>A work item</returns>
        public WorkItem GetWorkItem(int id)
        {
            WorkItemStore workItemStore = GetService<WorkItemStore>();
            return workItemStore.GetWorkItem(id);
        }

        /// <summary>
        /// Get a work item by a specific revision id
        /// </summary>
        /// <param name="id">Work Item ID</param>
        /// <param name="revision">Revision ID</param>
        /// <returns>A work item</returns>
        public WorkItem GetWorkItem(int id, int revision)
        {
            WorkItemStore workItemStore = GetService<WorkItemStore>();
            return workItemStore.GetWorkItem(id, revision);
        }

        /// <summary>
        /// Get all queries defined in the selected project
        /// </summary>
        /// <param name="project">TFS project name</param>
        /// <returns>A collection of queries</returns>
        public StoredQueryCollection GetQueries(string project)
        {
            Hashtable context = new Hashtable();
            WorkItemStore workItemStore = GetService<WorkItemStore>();
            return workItemStore.Projects[project].StoredQueries;
        }

        /// <summary>
        /// Execute a given query in the context of a given project
        /// </summary>
        /// <param name="project">TFS project name</param>
        /// <param name="storedQueryId">The GUI of the stored query</param>
        /// <returns>A collection of bugs</returns>
        public WorkItemCollection ExecuteQuery(string project, Guid storedQueryId)
        {
            Hashtable context = new Hashtable();
            context.Add("project", project);
            WorkItemStore workItemStore = GetService<WorkItemStore>();
            StoredQuery q = workItemStore.GetStoredQuery(storedQueryId);
            return workItemStore.Query(q.QueryText, context);
        }

        /// <summary>
        /// Create Sprint Backlog item that relates to a bug work item
        /// </summary>
        /// <param name="parentBacklogID"></param>
        /// <param name="relatedBugID"></param>
        public void CreateScrumBug(int sprintNumber,int parentBacklogID, int relatedBugID)
        {
            WorkItemStore workItemStore = GetService<WorkItemStore>();
            WorkItem bug = workItemStore.GetWorkItem(relatedBugID);
            WorkItem parent = workItemStore.GetWorkItem(parentBacklogID);

            WorkItemType bugWit = parent.Project.WorkItemTypes["Sprint Backlog Item"];

            WorkItem workItem = new WorkItem(bugWit);
            workItem.Title = bug.Title;
            workItem.IterationId = sprintNumber;//Cannot be inherited from parent since it could span sprints
            workItem.Fields[ConchangoFields.Team].Value = parent.Fields[ConchangoFields.Team].Value;

            //Support copying Baseline Work used in MSF process models when importing bugs
            if (bug.Fields.Contains("Microsoft.VSTS.Scheduling.BaselineWork") && bug.Fields["Microsoft.VSTS.Scheduling.BaselineWork"].Value != null && ((double)bug.Fields["Microsoft.VSTS.Scheduling.BaselineWork"].Value) > 0)
            {
                workItem.Fields[ConchangoFields.EstimatedEffort].Value = bug.Fields["Microsoft.VSTS.Scheduling.BaselineWork"].Value;
                workItem.Fields[ConchangoFields.WorkRemaining].Value = bug.Fields["Microsoft.VSTS.Scheduling.BaselineWork"].Value;
            }
            else
            {
                workItem.Fields[ConchangoFields.EstimatedEffort].Value = 6;
                workItem.Fields[ConchangoFields.WorkRemaining].Value = 6;
            }

            if (bug.Fields.Contains("Microsoft.VSTS.Common.Priority") && bug.Fields["Microsoft.VSTS.Common.Priority"].Value != null)
            {
                workItem.Fields[ConchangoFields.TaskPriority].Value = bug.Fields["Microsoft.VSTS.Common.Priority"].Value;
            }

            workItem.Links.Add(new RelatedLink(parentBacklogID));
            RelatedLink lnk = new RelatedLink(relatedBugID);
            lnk.Comment = "Scrum2BugLink";
            workItem.Links.Add(lnk);

            workItem.Save();

            TfsCacheDependency.FireTfsChanged(parent.Project.Name);//To make sure it's fired before the AJAX callback
        }

        /// <summary>
        /// Create a Sprint Backlog item
        /// </summary>
        /// <param name="parentBacklogID">The related backlog item</param>
        /// <param name="type">Type of Sprint Backlog item, for example Bug</param>
        /// <param name="title">The title</param>
        /// <param name="estimatedEffort">Estimated effort (can be null)</param>
        /// <param name="description">The description of the task</param>
        /// <returns>Work Item ID for the created task</returns>
        public int CreateScrumTask(int sprintNumber,int parentBacklogID, string type, string title, double? estimatedEffort, string description)
        {
            WorkItemStore workItemStore = GetService<WorkItemStore>();
            WorkItemType bugWit;

            WorkItem parent = workItemStore.GetWorkItem(parentBacklogID);
            WorkItem workItem;

            switch (type.ToLower())
            {
                case "task":
                    bugWit = parent.Project.WorkItemTypes["Sprint Backlog Item"];
                    workItem = new WorkItem(bugWit);
                    workItem.Title = title;
                    break;
                case "bug":
                    bugWit = parent.Project.WorkItemTypes["Sprint Backlog Item"];
                    workItem = new WorkItem(bugWit);
                    workItem.Title = SprintBacklogItem.BugTitlePrefix + title;
                    break;
                case "impediment":
                    bugWit = parent.Project.WorkItemTypes["Impediment"];
                    workItem = new WorkItem(bugWit);
                    workItem.Title = title;
                    break;
                default:
                    throw new Exception("Unknown scrum task: " + type);

            }

            workItem.IterationId = sprintNumber;//Cannot be inherited from parent since it could span sprints
            if (!String.IsNullOrEmpty(parent.AreaPath))
                workItem.AreaPath = parent.AreaPath;

            if (estimatedEffort.HasValue)
            {
                workItem.Fields[ConchangoFields.WorkRemaining].Value = estimatedEffort;
                workItem.Fields[ConchangoFields.EstimatedEffort].Value = estimatedEffort;
            }
            workItem.Fields[ConchangoFields.Team].Value = parent.Fields[ConchangoFields.Team].Value;
            workItem.Fields["System.Description"].Value = description;

            workItem.Links.Add(new RelatedLink(parentBacklogID));

            workItem.Save();

            TfsCacheDependency.FireTfsChanged(parent.Project.Name);//To make sure it's fired before the AJAX callback

            return workItem.Id;
        }

        /// <summary>
        /// Create a product backlog directly for one sprint
        /// </summary>
        /// <param name="tfsProject">The project</param>
        /// <param name="sprintNumber">Sprint Number</param>
        /// <param name="teamName">Team name (can be empty)</param>
        /// <param name="title">The title of the backlog</param>
        /// <param name="effortVal">The effort of the backlog (in points)</param>
        /// <param name="description">The description of the backlog</param>
        /// <returns></returns>
        public int CreateProductBacklog(string tfsProject, int sprintNumber, string teamName, string title, double? effortVal, string description)
        {
            WorkItemStore workItemStore = GetService<WorkItemStore>();
            WorkItemType bugWit = workItemStore.Projects[tfsProject].WorkItemTypes["Product Backlog Item"];

            WorkItem workItem = new WorkItem(bugWit);
            workItem.Title = title;
            workItem.IterationId = sprintNumber;
            if (effortVal.HasValue)
                workItem.Fields[ConchangoFields.EstimatedEffort].Value = effortVal;
            if (!String.IsNullOrEmpty(teamName))
                workItem.Fields[ConchangoFields.Team].Value = teamName;

            workItem.Fields["System.Description"].Value = description;

            workItem.Save();

            TfsCacheDependency.FireTfsChanged(tfsProject);//To make sure it's fired before the AJAX callback

            return workItem.Id;
        }

        /// <summary>
        /// Add a product backlog to a sprint
        /// </summary>
        /// <param name="id">The ID of the product backlog</param>
        /// <param name="sprintNumber">The ID of the sprint</param>
        public void AddProductBacklogToSprint(int id, int sprintNumber)
        {
            WorkItemStore workItemStore = GetService<WorkItemStore>();
            WorkItem wi = workItemStore.GetWorkItem(id);
            wi.IterationId = sprintNumber;
            wi.Save();

            TfsCacheDependency.FireTfsChanged(wi.Project.Name);//To make sure it's fired before the AJAX callback
        }

        /// <summary>
        /// Changes work remaining for a specific work item
        /// </summary>
        /// <param name="id">The work item ID</param>
        /// <param name="time">The time in hours</param>
        public void ChangeWorkRemaining(int id, float time)
        {
            WorkItemStore workItemStore = GetService<WorkItemStore>();
            WorkItem wi = workItemStore.GetWorkItem(id);
            wi.Fields[ConchangoFields.WorkRemaining].Value = time;
            wi.Save();

            TfsCacheDependency.FireTfsChanged(wi.Project.Name);//To make sure it's fired before the AJAX callback
        }

        /// <summary>
        /// Get all active product backlogs (found in active sprints with +-1 day interval)
        /// </summary>
        /// <param name="projectName"></param>
        /// <returns></returns>
        public ProductBacklogItem[] GetActiveProductBacklogs(string projectName)
        {
            Hashtable context = new Hashtable();
            context.Add("project", projectName);
            context.Add("start", DateTime.Today.AddDays(1));
            context.Add("stop", DateTime.Today.AddDays(-1));

            WorkItemStore workItemStore = GetService<WorkItemStore>();
            WorkItemCollection retVal = workItemStore.Query("SELECT " + ObjectFactory.CommonSelectFields + " FROM WorkItem WHERE [System.TeamProject] = @project AND [System.WorkItemType]='Sprint' AND [System.State]='In Progress' AND [Conchango.TeamSystem.Scrum.SprintStart]<=@start AND [Conchango.TeamSystem.Scrum.SprintEnd]>=@stop", context);

            Hashtable foundIterations = new Hashtable();
            string q = string.Empty;

            foreach (WorkItem wi in retVal)
            {
                if (foundIterations.Contains(wi.IterationId))
                    continue;

                foundIterations.Add(wi.IterationId, null);
                if (q.Length > 0)
                    q += " OR ";
                q += "[System.IterationId]=" + wi.IterationId.ToString();
            }

            if (foundIterations.Count == 0)
                return new ProductBacklogItem[] { };

            retVal = workItemStore.Query("SELECT " + ObjectFactory.ProductBacklogFields + " FROM WorkItem WHERE [System.TeamProject] = @project AND [System.WorkItemType]='Product Backlog Item' AND (" + q + ") ORDER BY [System.Title] ASC", context);

            ProductBacklogItem[] items = new ProductBacklogItem[retVal.Count];
            for (int i = 0; i < retVal.Count; i++)
            {
                items[i] = ObjectFactory.CreateProductBacklogItem(retVal[i], GetUser(projectName, retVal[i].Fields["System.AssignedTo"].Value));
            }

            return items;
        }

        /// <summary>
        /// Helper method that looks up a user in the list of contributors
        /// </summary>
        /// <param name="tfsProject">Name of project</param>
        /// <param name="displayName">Display name for user</param>
        /// <returns>The user or TfsUser.Empty if you pass in null or empty</returns>
        private User GetUser(string tfsProject, object displayNameObj)
        {
            string displayName = displayNameObj as string;
            if (String.IsNullOrEmpty(displayName))
                return User.Empty;

            if (_userLookup == null)
                _userLookup = GetProjectContributorsLookup(tfsProject);

            User user;
            if (_userLookup.TryGetValue(displayName, out user))
                return user;
            else
                return User.Empty;
        }

    }
}
